/*******************************************************************************
 * Copyright (c) 2011 LegSem.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl-3.0.txt
 * 
 * Contributors:
 *     LegSem - initial API and implementation
 ******************************************************************************/
package com.legsem.legstar.cci;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.resource.NotSupportedException;
import javax.resource.ResourceException;
import javax.resource.cci.Connection;
import javax.resource.cci.InteractionSpec;
import javax.resource.cci.Record;
import javax.resource.cci.ResourceWarning;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.legsem.legstar.jca.LegStarManagedConnection;
import com.legstar.coxb.transform.HostTransformException;
import com.legstar.coxb.transform.IHostToJavaTransformer;
import com.legstar.coxb.transform.IJavaToHostTransformer;
import com.legstar.host.invoke.HostInvoker;
import com.legstar.host.invoke.HostInvokerException;
import com.legstar.messaging.HostEndpoint;
import com.legstar.messaging.LegStarAddress;

/**
 * Implements an interaction with z/OS.
 * 
 */
public class LegStarInteractionImpl implements LegStarInteraction {

    private static final String CLOSED_ERROR = "Connection closed";
    private static final String EXECUTE_WITH_RECORDS_NOT_SUPPORTED = "execute() with records not supported";
    private static final String MISSING_HOSTPROGRAM = "The HostProgram structure is missing from interaction specifications";
    private static final String MISSING_PROGRAM_NAME = "The HostProgram structure does not contain a program name";
    private static final String MISSING_INPUT_TRANSFORMER = "The input Transformer was not provided";
    private static final String MISSING_OUTPUT_TRANSFORMER = "The output Transformer was not provided";
    private static final String MISSING_INPUT_TRANSFORMERS = "The input Transformers map was not provided";
    private static final String MISSING_OUTPUT_TRANSFORMERS = "The output Transformers map was not provided";
    private static final String NO_NAMED_INPUT_TRANSFORMER = "No input transformer was specified of input named ";
    private static final String NO_NAMED_OUTPUT_TRANSFORMER = "No output transformer was specified of output named ";

    private LegStarConnection connection;
    private LegStarManagedAccessStrategy accessStrategy;
    private HostEndpoint endpoint;
    private boolean valid;

    /** Logger. */
    private final Log log = LogFactory.getLog(getClass());

    /**
     * Because JCA requires that an Interaction maintain its association with
     * the connection handle used to create it, the constructor requires that a
     * Connection be passed in. A flag is set during instantiation to indicate
     * that this is a valid Interaction, that is, not closed. In other methods,
     * where appropriate, this flag is checked to determine if the requested
     * action can be carried out.
     */
    public LegStarInteractionImpl(Connection connection) {
        super();
        this.connection = (LegStarConnection) connection;
        LegStarManagedConnection managedConnection = this.connection
                .getManagedConnection();
        accessStrategy = new LegStarManagedAccessStrategy(
                managedConnection.getHostConnection());
        endpoint = managedConnection.getHostEndpoint();
        valid = true;
    }

    public void clearWarnings() throws ResourceException {

    }

    public void close() throws ResourceException {
        if (log.isDebugEnabled()) {
            log.debug("Closing interaction");
        }
        connection = null;
        accessStrategy = null;
        endpoint = null;
        valid = false;
    }

    public Record execute(InteractionSpec arg0, Record arg1)
            throws ResourceException {
        throw new NotSupportedException(EXECUTE_WITH_RECORDS_NOT_SUPPORTED);
    }

    public boolean execute(InteractionSpec ispec, Record input, Record output)
            throws ResourceException {
        throw new NotSupportedException(EXECUTE_WITH_RECORDS_NOT_SUPPORTED);
    }

    public Object execute(LegStarSingleIOInteractionSpec ispec, Object input)
            throws ResourceException {

        check(ispec);

        try {
            return execute(getHostInvoker(ispec), getHostCharset(ispec), ispec,
                    input);

        } catch (HostInvokerException e) {
            connection.getManagedConnection().onError(e);
            throw new LegStarHostInvokeException(e);
        } catch (HostTransformException e) {
            connection.getManagedConnection().onError(e);
            throw new LegStarHostTransformException(e);
        }

    }

    public Map < String, Object > execute(LegStarMultiIOInteractionSpec ispec,
            Map < String, Object > input) throws ResourceException {

        check(ispec);

        try {
            return execute(getHostInvoker(ispec), getHostCharset(ispec), ispec,
                    input);
        } catch (HostInvokerException e) {
            connection.getManagedConnection().onError(e);
            throw new LegStarHostInvokeException(e);
        } catch (HostTransformException e) {
            connection.getManagedConnection().onError(e);
            throw new LegStarHostTransformException(e);
        }
    }

    /**
     * Create a LegStar invoker for the target endpoint and program.
     * 
     * @param ispec the interaction specs
     * @return an invoker ready to execute the remote program
     * @throws HostInvokerException if invoker cannot be created
     */
    protected HostInvoker getHostInvoker(LegStarInteractionSpec ispec)
            throws HostInvokerException {

        LegStarAddress address = new LegStarAddress(getEndpoint());
        return LegStarManagedHostInvokerFactory.createHostInvoker(address,
                getAccessStrategy(), ispec.getHostProgram());
    }

    /**
     * The Host character can be used is specified in the specs otherwise will
     * be picked up from the configuration.
     * 
     * @param ispec the interaction specs
     * @return a host character set
     */
    protected String getHostCharset(LegStarInteractionSpec ispec) {
        return (ispec.getHostCharset() == null) ? getEndpoint()
                .getHostCharset() : ispec.getHostCharset();
    }

    /**
     * Transform input object to host data, execute the remote program and
     * transform the data back to a reply object.
     * 
     * @param invoker the legstar host invoker
     * @param hostCharset the host character set
     * @param ispec the interaction specs
     * @param input the input object
     * @return the output reply object
     * @throws HostTransformException if transformation from java to host fails
     * @throws HostInvokerException if execution of remote program fails
     */
    protected Object execute(HostInvoker invoker, String hostCharset,
            LegStarSingleIOInteractionSpec ispec, Object input)
            throws HostTransformException, HostInvokerException {

        byte[] requestBytes = ispec.getInputTransformer().transform(input,
                hostCharset);
        if (log.isDebugEnabled()) {
            log.debug("About to send " + requestBytes.length + " host bytes");
        }
        byte[] replyBytes = invoker.invoke(ispec.getInteractionId(),
                requestBytes);

        if (log.isDebugEnabled()) {
            log.debug("Received " + replyBytes.length + " host bytes");
        }
        return ispec.getOutputTransformer().transform(replyBytes, hostCharset);
    }

    /**
     * Transform input objects to host data, execute the remote program and
     * transform the data back to reply objects.
     * 
     * @param invoker the legstar host invoker
     * @param hostCharset the host character set
     * @param ispec the interaction specs
     * @param input a map of input objects
     * @return a map of reply objects
     * @throws HostTransformException if transformation from java to host fails
     * @throws HostInvokerException if execution of remote program fails
     */
    protected Map < String, Object > execute(HostInvoker invoker,
            String hostCharset, LegStarMultiIOInteractionSpec ispec,
            Map < String, Object > input) throws HostInvokerException,
            HostTransformException {

        Map < String, byte[] > inputParts = new HashMap < String, byte[] >();
        for (Entry < String, Object > entry : input.entrySet()) {
            IJavaToHostTransformer tf = ispec.getInputTransformers().get(
                    entry.getKey());
            if (tf == null) {
                throw new HostTransformException(NO_NAMED_INPUT_TRANSFORMER
                        + entry.getKey());
            }
            byte[] requestBytes = tf.transform(entry.getValue(), hostCharset);
            if (log.isDebugEnabled()) {
                log.debug("About to send " + requestBytes.length
                        + " host bytes, for " + entry.getKey());
            }
            inputParts.put(entry.getKey(), requestBytes);
        }

        Map < String, byte[] > outputParts = invoker.invoke(
                ispec.getInteractionId(), inputParts);

        Map < String, Object > output = new HashMap < String, Object >();
        for (Entry < String, byte[] > entry : outputParts.entrySet()) {
            if (log.isDebugEnabled()) {
                log.debug("Received " + entry.getValue().length
                        + " host bytes, for " + entry.getKey());
            }
            IHostToJavaTransformer tf = ispec.getOutputTransformers().get(
                    entry.getKey());
            if (tf == null) {
                throw new HostTransformException(NO_NAMED_OUTPUT_TRANSFORMER
                        + entry.getKey());
            }
            Object replyObject = tf.transform(entry.getValue(), hostCharset);
            output.put(entry.getKey(), replyObject);
        }
        return output;

    }

    /**
     * Check the state and interaction specs.
     * 
     * @param ispec the proposed interaction spec
     */
    protected void check(LegStarSingleIOInteractionSpec ispec)
            throws ResourceException {

        if (!valid) {
            throw new javax.resource.spi.IllegalStateException(CLOSED_ERROR);
        }

        checkCommonInteractionSpec(ispec);

        if (ispec.getInputTransformer() == null) {
            throw new ResourceException(MISSING_INPUT_TRANSFORMER);
        }
        if (ispec.getOutputTransformer() == null) {
            throw new ResourceException(MISSING_OUTPUT_TRANSFORMER);
        }

        if (log.isDebugEnabled()) {
            log.debug("About to execute interaction " + ispec);
        }

    }

    /**
     * Check that interaction spec are complete.
     * 
     * @param ispec the proposed interaction spec
     */
    protected void check(LegStarMultiIOInteractionSpec ispec)
            throws ResourceException {

        checkCommonInteractionSpec(ispec);

        if (ispec.getInputTransformers() == null) {
            throw new ResourceException(MISSING_INPUT_TRANSFORMERS);
        }
        if (ispec.getOutputTransformers() == null) {
            throw new ResourceException(MISSING_OUTPUT_TRANSFORMERS);
        }

    }

    /**
     * The interaction spec contains some important info about the target
     * program, check if the properties are valid.
     * 
     * @param ispec the proposed interaction spec
     */
    protected void checkCommonInteractionSpec(LegStarInteractionSpec ispec)
            throws ResourceException {
        if (ispec.getHostProgram() == null) {
            throw new ResourceException(MISSING_HOSTPROGRAM);
        }
        if (ispec.getHostProgram().getName() == null
                || ispec.getHostProgram().getName().length() == 0) {
            throw new ResourceException(MISSING_PROGRAM_NAME);
        }

    }

    public Connection getConnection() {
        return connection;
    }

    public ResourceWarning getWarnings() throws ResourceException {
        return null;
    }

    public LegStarManagedAccessStrategy getAccessStrategy() {
        return accessStrategy;
    }

    public HostEndpoint getEndpoint() {
        return endpoint;
    }

}
